/*
 * Copyright 2019. Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.android.apps.santatracker.doodles.penguinswim;

import android.content.res.Resources;
import android.graphics.Canvas;
import com.google.android.apps.santatracker.doodles.shared.CallbackProcess;
import com.google.android.apps.santatracker.doodles.shared.Debug;
import com.google.android.apps.santatracker.doodles.shared.EventBus;
import com.google.android.apps.santatracker.doodles.shared.ProcessChain;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.shared.WaitProcess;
import com.google.android.apps.santatracker.doodles.shared.actor.MultiSpriteActor;
import com.google.android.apps.santatracker.doodles.shared.animation.AnimatedSprite;
import com.google.android.apps.santatracker.doodles.shared.animation.AnimatedSprite.AnimatedSpriteListener;
import com.google.android.apps.santatracker.doodles.shared.physics.Polygon;
import com.google.android.apps.santatracker.doodles.shared.physics.Util;
import com.google.android.apps.santatracker.doodles.shared.views.GameFragment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.json.JSONObject;

/** The player-controlled swimmer in the swimming game. */
public class SwimmerActor extends BoundingBoxSpriteActor {
    public static final float SWIMMER_SCALE = 1.6f;
    public static final int DIVE_DURATION_MS = 1500;
    public static final int DIVE_COOLDOWN_MS = 5000;
    public static final String SWIMMER_ACTOR_TYPE = "swimmer";
    public static final String KICKOFF_IDLE_SPRITE = "kickoff_idle";
    public static final String KICKOFF_START_SPRITE = "kickoff_start";
    public static final String RINGS_SPRITE = "rings";
    public static final String SWIM_LOOP_SPRITE = "swimming";
    public static final String CAN_COLLIDE_SPRITE = "can_collide";
    public static final String FREEZE_SPRITE = "freeze";
    public static final String DIVE_DOWN_SPRITE = "dive";
    public static final String UNDER_LOOP_SPRITE = "under_loop";
    public static final String RISE_UP_SPRITE = "rise_up";
    public static final float KICKOFF_IDLE_Y_OFFSET = -240;
    private static final String TAG = SwimmerActor.class.getSimpleName();
    private static final float ACCELERATION_Y = -500;
    private static final float MIN_SPEED = 400;
    private static final float DEFAULT_MAX_SPEED = 800;
    private static final long SPEED_STEP_DURATION_MS = 10000;
    private static final float TILT_VELOCITY = 10000;
    private static final Vector2D[] VERTEX_OFFSETS = {
        Vector2D.get(0, 0), Vector2D.get(96, 0),
        Vector2D.get(96, 90), Vector2D.get(0, 90)
    };

    private static final Map<String, Vector2D> OFFSET_MAP;

    static {
        OFFSET_MAP = new HashMap<>();
        OFFSET_MAP.put(KICKOFF_IDLE_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(KICKOFF_START_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(RINGS_SPRITE, Vector2D.get(-60, -20)); // TODO
        OFFSET_MAP.put(SWIM_LOOP_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(CAN_COLLIDE_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(FREEZE_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(DIVE_DOWN_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(UNDER_LOOP_SPRITE, Vector2D.get(0, 0));
        OFFSET_MAP.put(RISE_UP_SPRITE, Vector2D.get(0, 0));
    }

    public boolean controlsEnabled = true;
    public boolean isInvincible = false;
    public boolean isUnderwater = false;
    public boolean isDead = false;

    private MultiSpriteActor multiSpriteActor;
    private AnimatedSprite canCollideSprite;
    private AnimatedSprite freezeSprite;
    private String collidedObjectType;

    private AnimatedSprite ringsSprite;
    private Vector2D ringsSpriteOffset;

    private float restartSpeed = MIN_SPEED;
    private float maxSpeed = DEFAULT_MAX_SPEED;
    private long currentSpeedStepTime = 0;

    private boolean diveEnabled = false;
    private float targetX;

    private List<ProcessChain> processChains = new ArrayList<>();

    public SwimmerActor(Polygon collisionBody, MultiSpriteActor spriteActor) {
        super(
                collisionBody,
                spriteActor,
                Vector2D.get(OFFSET_MAP.get(KICKOFF_IDLE_SPRITE)).scale(SWIMMER_SCALE),
                SWIMMER_ACTOR_TYPE);

        multiSpriteActor = spriteActor;
        canCollideSprite = multiSpriteActor.sprites.get(CAN_COLLIDE_SPRITE);
        canCollideSprite.setLoop(false);
        freezeSprite = multiSpriteActor.sprites.get(FREEZE_SPRITE);
        freezeSprite.setLoop(false);
        multiSpriteActor
                .sprites
                .get(DIVE_DOWN_SPRITE)
                .addListener(
                        new AnimatedSpriteListener() {
                            @Override
                            public void onLoop() {
                                zIndex = -3;
                                setSprite(UNDER_LOOP_SPRITE);
                            }
                        });
        multiSpriteActor
                .sprites
                .get(RISE_UP_SPRITE)
                .addListener(
                        new AnimatedSpriteListener() {
                            @Override
                            public void onLoop() {
                                zIndex = 0;
                                setSprite(SWIM_LOOP_SPRITE);
                                EventBus.getInstance()
                                        .sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_dive_up);
                            }
                        });
        multiSpriteActor
                .sprites
                .get(SWIM_LOOP_SPRITE)
                .addListener(
                        new AnimatedSpriteListener() {
                            @Override
                            public void onFrame(int index) {
                                if (index == 5 || index == 13) {
                                    EventBus.getInstance()
                                            .sendEvent(
                                                    EventBus.PLAY_SOUND,
                                                    R.raw.swimming_ice_splash_a);
                                }
                            }
                        });

        ringsSprite = multiSpriteActor.sprites.get(RINGS_SPRITE);
        ringsSprite.setPaused(true);
        ringsSprite.setHidden(true);
        ringsSprite.setScale(SWIMMER_SCALE, SWIMMER_SCALE);
        ringsSprite.addListener(
                new AnimatedSpriteListener() {
                    @Override
                    public void onLoop() {
                        ringsSprite.setHidden(true);
                        ringsSprite.setPaused(true);
                    }
                });
        ringsSpriteOffset = Vector2D.get(OFFSET_MAP.get(RINGS_SPRITE)).scale(SWIMMER_SCALE);

        multiSpriteActor
                .sprites
                .get(KICKOFF_START_SPRITE)
                .addListener(
                        new AnimatedSpriteListener() {
                            @Override
                            public void onLoop() {
                                diveEnabled = true;
                                diveDown();
                            }

                            @Override
                            public void onFrame(int index) {
                                if (index == 3) {
                                    velocity.set(0, -restartSpeed);
                                }
                            }
                        });

        zIndex = 0;
        alpha = 1.0f;
        scale = SWIMMER_SCALE;
        targetX = position.x;
    }

    public static final SwimmerActor create(
            Vector2D position, Resources res, final GameFragment gameFragment) {
        if (gameFragment.isDestroyed) {
            return null;
        }
        Map<String, AnimatedSprite> spriteMap = new HashMap<>();
        spriteMap.put(
                KICKOFF_IDLE_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_idle));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                KICKOFF_START_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_start));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                RINGS_SPRITE, AnimatedSprite.fromFrames(res, PenguinSwimSprites.swimming_rings));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                SWIM_LOOP_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_swimming));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                CAN_COLLIDE_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_dazed));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                FREEZE_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_frozen));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                DIVE_DOWN_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_descending));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                UNDER_LOOP_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_swimmingunderwater));
        if (gameFragment.isDestroyed) {
            return null;
        }
        spriteMap.put(
                RISE_UP_SPRITE,
                AnimatedSprite.fromFrames(res, PenguinSwimSprites.penguin_swim_ascending));
        if (gameFragment.isDestroyed) {
            return null;
        }
        MultiSpriteActor spriteActor =
                new MultiSpriteActor(spriteMap, KICKOFF_IDLE_SPRITE, position, Vector2D.get(0, 0));
        if (gameFragment.isDestroyed) {
            return null;
        }

        return new SwimmerActor(
                getBoundingBox(position, VERTEX_OFFSETS, SWIMMER_SCALE), spriteActor);
    }

    @Override
    public void update(float deltaMs) {
        super.update(deltaMs);

        ProcessChain.updateChains(processChains, deltaMs);

        // Update x position based on tilt.
        float frameVelocityX = TILT_VELOCITY * deltaMs / 1000;
        float positionDeltaX = targetX - position.x;
        if (Math.abs(positionDeltaX) < frameVelocityX) {
            // We will overshoot if we apply the frame velocity. Just go straight to the target
            // position.
            moveTo(targetX, position.y);
        } else {
            moveTo(position.x + Math.signum(positionDeltaX) * frameVelocityX, position.y);
        }

        // Update acceleration and frame rate if necessary.
        if (velocity.getLength() > 1) {
            velocity.y = Math.max(-maxSpeed, velocity.y + ACCELERATION_Y * deltaMs / 1000);
            if (velocity.y == -maxSpeed) {
                multiSpriteActor.sprites.get(SWIM_LOOP_SPRITE).setFPS(24);
            } else {
                multiSpriteActor.sprites.get(SWIM_LOOP_SPRITE).setFPS(48);
            }
            currentSpeedStepTime += deltaMs;
            if (currentSpeedStepTime > SPEED_STEP_DURATION_MS) {
                maxSpeed += 500;
                currentSpeedStepTime = 0;
            }
        }

        ringsSprite.update(deltaMs);
        ringsSprite.setPosition(
                spriteActor.position.x + ringsSpriteOffset.x,
                spriteActor.position.y + ringsSpriteOffset.y);
    }

    @Override
    public void draw(Canvas canvas) {
        if (hidden) {
            return;
        }
        spriteActor.draw(
                canvas,
                spriteOffset.x,
                spriteOffset.y,
                spriteActor.sprite.frameWidth * scale,
                spriteActor.sprite.frameHeight * scale);
        ringsSprite.draw(canvas);

        if (Debug.DRAW_COLLISION_BOUNDS) {
            collisionBody.draw(canvas);
        }
    }

    @Override
    public JSONObject toJSON() {
        return null;
    }

    public void setSprite(String key) {
        multiSpriteActor.setSprite(key);
        spriteOffset.set(OFFSET_MAP.get(key)).scale(SWIMMER_SCALE);
    }

    public void moveTo(float x, float y) {
        collisionBody.moveTo(x, y);
        position.set(x, y);
        spriteActor.position.set(x, y);
        targetX = x;
    }

    public void updateTargetPositionFromTilt(Vector2D tilt, float levelWidth) {
        if (controlsEnabled) {
            // Decrease the amount of tilt necessary to move the swimmer.
            float tiltPercentage = (float) (tilt.x / (Math.PI / 2));
            tiltPercentage *= 2.5f;

            int levelPadding = 60;
            targetX =
                    Util.clamp(
                            (levelWidth / 2)
                                    - (collisionBody.getWidth() / 2)
                                    + (tiltPercentage * levelWidth / 2),
                            0 - collisionBody.getWidth() + levelPadding,
                            levelWidth - (2 * collisionBody.getWidth()) - levelPadding);
        }
    }

    public void startSwimming() {
        setSprite(KICKOFF_START_SPRITE);
    }

    public void collide(String objectType) {
        restartSpeed = Math.max(MIN_SPEED, Math.abs(velocity.y / 4));
        maxSpeed = restartSpeed;
        currentSpeedStepTime = 0;
        moveTo(positionBeforeFrame.x, positionBeforeFrame.y);
        velocity.set(0, 0);
        controlsEnabled = false;

        if (objectType.equals(DUCK) || objectType.equals(ICE_CUBE)) {
            EventBus.getInstance().sendEvent(EventBus.SHAKE_SCREEN);
            EventBus.getInstance().sendEvent(EventBus.VIBRATE);

            if (objectType.equals(DUCK)) {
                setSprite(SwimmerActor.CAN_COLLIDE_SPRITE);
                EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_duck_collide);
            } else { // Ice cube.
                setSprite(SwimmerActor.FREEZE_SPRITE);
                EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_ice_collide);
            }
        } else { // Octopus.
            // Just play the sound for the octopus. It vibrates later (when it actually grabs the
            // lemon).
            EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_grab);
        }
        collidedObjectType = objectType;
        isDead = true;
    }

    public void endGameWithoutCollision() {
        controlsEnabled = false;
        collidedObjectType = HAND_GRAB;
        isDead = true;
    }

    public String getCollidedObjectType() {
        return collidedObjectType;
    }

    public void diveDown() {
        if (controlsEnabled && diveEnabled) {
            EventBus.getInstance().sendEvent(EventBus.SWIMMING_DIVE);
            EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_dive_down);
            ringsSprite.setHidden(false);
            ringsSprite.setPaused(false);
            isUnderwater = true;
            setSprite(DIVE_DOWN_SPRITE);
            diveEnabled = false;

            ProcessChain waitThenRiseUp =
                    new WaitProcess(DIVE_DURATION_MS)
                            .then(
                                    new CallbackProcess() {
                                        @Override
                                        public void updateLogic(float deltaMs) {
                                            isUnderwater = false;
                                            setSprite(RISE_UP_SPRITE);
                                        }
                                    })
                            .then(new WaitProcess(DIVE_COOLDOWN_MS))
                            .then(
                                    new CallbackProcess() {
                                        @Override
                                        public void updateLogic(float deltaMs) {
                                            diveEnabled = true;
                                        }
                                    });
            processChains.add(waitThenRiseUp);
        }
    }
}
